//----------------------------------------------------------------------------------------------------------------------
// Copyright (c) 2012 James Whitworth
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//----------------------------------------------------------------------------------------------------------------------
#include <gmock/gmock.h>

#include <sqbind/sqbClassDefinition.h>
#include <sqbind/sqbClassHelpers.h>
#include <sqbind/sqbStackUtils.h>

#include "fixtures/SquirrelFixture.h"
#include "mocks/MockReleaseHook.h"
//----------------------------------------------------------------------------------------------------------------------

// conditional expression is constant
#pragma warning(disable: 4127)

typedef SquirrelFixture ClassHelpersTest;
typedef ClassHelpersTest ClassHelpersDeathTest;

namespace
{
class CopyablePodType {};
class NonCopyableNonPodType
{
public:
  NonCopyableNonPodType() { }

  // copy construction and assignment are disabled
  //
  NonCopyableNonPodType( const NonCopyableNonPodType & ) = delete;
  NonCopyableNonPodType &operator = ( const NonCopyableNonPodType & ) = delete;

  // Virtual destructor to make it a non pod type
  virtual ~NonCopyableNonPodType() { }
};
}

SQBIND_TYPE_NON_COPY_CONSTRUCTIBLE(NonCopyableNonPodType);
SQBIND_TYPE_NON_COPY_ASSIGNABLE(NonCopyableNonPodType);

SQBIND_DECLARE_TYPEINFO(CopyablePodType, CopyablePodType);
SQBIND_DECLARE_TYPEINFO(NonCopyableNonPodType, NonCopyableNonPodType);

//----------------------------------------------------------------------------------------------------------------------
template<typename TypeParam>
class ClassHelpersTypedDeathTest : public SquirrelFixture
{
public:
  enum
  {
    kIsCopyableType = std::is_copy_constructible<TypeParam>::value,
    kIsPodType = std::is_pod<TypeParam>::value,
  };
};

//----------------------------------------------------------------------------------------------------------------------
typedef ::testing::Types<CopyablePodType, NonCopyableNonPodType> ClassHelperTypes;

TYPED_TEST_CASE(ClassHelpersTypedDeathTest, ClassHelperTypes);

//----------------------------------------------------------------------------------------------------------------------
TEST_F(ClassHelpersTest, TestNativeTypeOf)
{
  const SQChar* typestring;

  EXPECT_EQ(1, sqb::TypeOf<CopyablePodType>(m_vm));
  EXPECT_SQ_SUCCEEDED(m_vm, sq_getstring(m_vm, -1, &typestring));
  EXPECT_STREQ(_SC("CopyablePodType"), typestring);
  sq_poptop(m_vm);

  EXPECT_EQ(1, sqb::TypeOf<NonCopyableNonPodType>(m_vm));
  EXPECT_SQ_SUCCEEDED(m_vm, sq_getstring(m_vm, -1, &typestring));
  EXPECT_STREQ(_SC("NonCopyableNonPodType"), typestring);
  sq_poptop(m_vm);
}

//----------------------------------------------------------------------------------------------------------------------
TEST_F(ClassHelpersTest, TestCreateNativeClassInstance)
{
  SQInteger expectedTop = sq_gettop(m_vm);

  HSQOBJECT klass;
  sq_resetobject(&klass);

  EXPECT_FALSE(sqb::CreateNativeClassInstance(m_vm, klass, nullptr));
  EXPECT_EQ(expectedTop, sq_gettop(m_vm));

  EXPECT_SQ_SUCCEEDED(m_vm, sq_newclass(m_vm, SQFalse));
  EXPECT_SQ_SUCCEEDED(m_vm, sq_getstackobj(m_vm, -1, &klass));
  sq_addref(m_vm, &klass);
  sq_poptop(m_vm);

  EXPECT_TRUE(sqb::CreateNativeClassInstance(m_vm, klass, nullptr));
  EXPECT_EQ(expectedTop + 1, sq_gettop(m_vm));
  EXPECT_EQ(OT_INSTANCE, sq_gettype(m_vm, -1));
  sq_poptop(m_vm);

  ::testing::StrictMock<MockReleaseHook> mock;
  MockReleaseHook::m_instance = &mock;

  EXPECT_CALL(mock, ClassReleaseHook(nullptr, 0))
    .Times(1);

  EXPECT_TRUE(sqb::CreateNativeClassInstance(m_vm, klass, &MockReleaseHook::ReleaseHook));
  EXPECT_EQ(expectedTop + 1, sq_gettop(m_vm));
  EXPECT_EQ(OT_INSTANCE, sq_gettype(m_vm, -1));
  sq_poptop(m_vm);

  sq_collectgarbage(m_vm);
}

//----------------------------------------------------------------------------------------------------------------------
TYPED_TEST(ClassHelpersTypedDeathTest, TestClassUserDataConstructor)
{
  sqb::ClassTypeTag<TypeParam>::Get()->SetReleaseHook(&MockReleaseHook::ReleaseHook);

  ::testing::StrictMock<MockReleaseHook> mock;
  MockReleaseHook::m_instance = &mock;

#if SQBIND_ASSERTS_ENABLED
  EXPECT_DEATH(sqb::ClassUserDataClassDefinition<TypeParam>::DefaultConstructor(m_vm), "");

  EXPECT_SQ_SUCCEEDED(m_vm, sq_newclass(m_vm, SQFalse));
  EXPECT_SQ_SUCCEEDED(m_vm, sq_createinstance(m_vm, -1));
  sq_remove(m_vm, 1);
  EXPECT_DEATH(sqb::ClassUserDataClassDefinition<TypeParam>::DefaultConstructor(m_vm), "");
  sq_poptop(m_vm);
#endif

  EXPECT_SQ_SUCCEEDED(m_vm, sq_newclass(m_vm, SQFalse));
  EXPECT_SQ_SUCCEEDED(m_vm, sq_setclassudsize(m_vm, -1, sizeof(TypeParam)));

  EXPECT_SQ_SUCCEEDED(m_vm, sq_createinstance(m_vm, -1));
  sq_remove(m_vm, 1);
  EXPECT_EQ(1, sqb::ClassUserDataClassDefinition<TypeParam>::DefaultConstructor(m_vm));
  EXPECT_EQ(OT_INSTANCE, sq_gettype(m_vm, -1));

  EXPECT_CALL(mock, ClassReleaseHook(::testing::_, 0))
    .Times(1);
  sq_poptop(m_vm);
  EXPECT_SQ_SUCCEEDED(m_vm, sq_collectgarbage(m_vm));

  if (kIsCopyableType)
  {
    EXPECT_SQ_SUCCEEDED(m_vm, sq_newclass(m_vm, SQFalse));
    EXPECT_SQ_SUCCEEDED(m_vm, sq_setclassudsize(m_vm, -1, sizeof(TypeParam)));

    EXPECT_SQ_SUCCEEDED(m_vm, sq_createinstance(m_vm, -1));

    // add instance argument for the copy constructor
    //
    EXPECT_SQ_SUCCEEDED(m_vm, sq_createinstance(m_vm, -2));
    SQUserPointer copy;
    EXPECT_SQ_SUCCEEDED(m_vm, sq_getinstanceup(m_vm, -1, &copy, nullptr));
    new (copy) TypeParam;

    sq_remove(m_vm, 1);
    EXPECT_SQ_FAILED(sqb::ClassUserDataClassDefinition<TypeParam>::DefaultConstructor(m_vm));
    
    EXPECT_SQ_SUCCEEDED(m_vm, sq_getclass(m_vm, -1));
    EXPECT_SQ_SUCCEEDED(m_vm, sq_settypetag(m_vm, -1, sqb::ClassTypeTag<TypeParam>::Get()));
    sq_poptop(m_vm);

    EXPECT_EQ(1, sqb::ClassUserDataClassDefinition<TypeParam>::DefaultConstructor(m_vm));
    EXPECT_EQ(OT_INSTANCE, sq_gettype(m_vm, -1));

    EXPECT_CALL(mock, ClassReleaseHook(::testing::_, 0))
      .Times(1);
    sq_pop(m_vm, 2);
    EXPECT_SQ_SUCCEEDED(m_vm, sq_collectgarbage(m_vm));
  }

  sqb::ClassTypeTag<TypeParam>::Get()->SetReleaseHook(nullptr);
}

//----------------------------------------------------------------------------------------------------------------------
TYPED_TEST(ClassHelpersTypedDeathTest, TestSqMallocConstructor)
{
  sqb::ClassTypeTag<TypeParam>::Get()->SetReleaseHook(&MockReleaseHook::ReleaseHook);

  ::testing::StrictMock<MockReleaseHook> mock;
  MockReleaseHook::m_instance = &mock;

#if SQBIND_ASSERTS_ENABLED
  EXPECT_DEATH(sqb::SqMallocClassDefinition<TypeParam>::DefaultConstructor(m_vm), "");
#endif

  EXPECT_SQ_SUCCEEDED(m_vm, sq_newclass(m_vm, SQFalse));
  EXPECT_SQ_SUCCEEDED(m_vm, sq_createinstance(m_vm, -1));
  sq_remove(m_vm, 1);
  EXPECT_EQ(1, sqb::SqMallocClassDefinition<TypeParam>::DefaultConstructor(m_vm));
  EXPECT_EQ(OT_INSTANCE, sq_gettype(m_vm, -1));

  EXPECT_CALL(mock, ClassReleaseHook(::testing::_, 0))
    .Times(1);
  sq_poptop(m_vm);
  EXPECT_SQ_SUCCEEDED(m_vm, sq_collectgarbage(m_vm));

  if (kIsCopyableType)
  {
    TypeParam copy;
    EXPECT_SQ_SUCCEEDED(m_vm, sq_newclass(m_vm, SQFalse));

    EXPECT_SQ_SUCCEEDED(m_vm, sq_createinstance(m_vm, -1));

    // add instance argument for the copy constructor
    //
    EXPECT_SQ_SUCCEEDED(m_vm, sq_createinstance(m_vm, -2));
    EXPECT_SQ_SUCCEEDED(m_vm, sq_setinstanceup(m_vm, -1, &copy));

    sq_remove(m_vm, 1);
    EXPECT_SQ_FAILED(sqb::SqMallocClassDefinition<TypeParam>::DefaultConstructor(m_vm));

    EXPECT_SQ_SUCCEEDED(m_vm, sq_getclass(m_vm, -1));
    EXPECT_SQ_SUCCEEDED(m_vm, sq_settypetag(m_vm, -1, sqb::ClassTypeTag<TypeParam>::Get()));
    sq_poptop(m_vm);

    EXPECT_EQ(1, sqb::SqMallocClassDefinition<TypeParam>::DefaultConstructor(m_vm));
    EXPECT_EQ(OT_INSTANCE, sq_gettype(m_vm, -1));

    EXPECT_CALL(mock, ClassReleaseHook(::testing::_, 0))
      .Times(1);
    sq_pop(m_vm, 2);
    EXPECT_SQ_SUCCEEDED(m_vm, sq_collectgarbage(m_vm));
  }

  sqb::ClassTypeTag<TypeParam>::Get()->SetReleaseHook(nullptr);
}

namespace
{
class NonPodType
{
public:
  MOCK_METHOD0(Destructor, void());
  virtual ~NonPodType() { Destructor(); }
};

class FakePodType
{
public:
  MOCK_METHOD0(Destructor, void());
  virtual ~FakePodType() { Destructor(); }
};
}

// have to manually override std::is_pod for the test as types with a destructor aren't pod_types
namespace std
{
template<>
struct is_pod<FakePodType> : true_type {};
}

//----------------------------------------------------------------------------------------------------------------------
TEST_F(ClassHelpersTest, TestClassUserDataReleaseHook)
{
  uint8_t buffer[sizeof(::testing::StrictMock<NonPodType>)];
  ::testing::StrictMock<NonPodType>* mock = new (buffer) ::testing::StrictMock<NonPodType>;

  // non-pod types should call the destructor
  //
  EXPECT_CALL(*mock, Destructor())
    .Times(1);

  sqb::ClassUserDataClassDefinition<NonPodType>::DefaultReleaseHook(mock, 0);

  // there is no ClassUserData implementation for pod types as it has nothing to do.
}

//----------------------------------------------------------------------------------------------------------------------
TEST_F(ClassHelpersTest, TestSqFreeReleaseHook)
{
  {
    void *buffer = sq_malloc(sizeof(::testing::StrictMock<NonPodType>));
    ::testing::StrictMock<NonPodType> *mock = new (buffer) ::testing::StrictMock<NonPodType>;
    
    // non-pod types should call the destructor
    //
    EXPECT_CALL(*mock, Destructor())
      .Times(1);

    sqb::SqMallocClassDefinition<NonPodType>::DefaultReleaseHook(mock, sizeof(::testing::StrictMock<NonPodType>));
  }

  {
    void *buffer = sq_malloc(sizeof(::testing::StrictMock<NonPodType>));
    ::testing::StrictMock<FakePodType> *mock = new (buffer) ::testing::StrictMock<FakePodType>;
    // pod types shouldn't call the destructor but we need to manually call it to properly clean up the mock.
    //
    EXPECT_CALL(*mock, Destructor())
      .Times(1);
    mock->~StrictMock<FakePodType>();

    sqb::SqMallocClassDefinition<FakePodType>::DefaultReleaseHook(mock, sizeof(::testing::StrictMock<NonPodType>));
  }
}
